#pragma once

#ifndef ATTRIB_HPP
#define ATTRIB_HPP

#include "MeshLoader.hpp"
#include "Platform.hpp"
#include <array>
#include <cassert>
#include <cstdint>
#include <optional>
#include <string>


namespace Data {

class ProgramState;

static constexpr auto MAX_VERTEX_ATTRIBUTE_NUM = 16;
static constexpr GLenum ERR_ENUM = 0u;

enum class AttriDataType { FLOAT32, INT32, UINT32, INT16, UINT16, INT8, UINT8 };

static inline GLenum GetGLType(AttriDataType type) {
  static GLenum table[] = {GL_FLOAT,          GL_INT,  GL_UNSIGNED_INT, GL_SHORT,
                           GL_UNSIGNED_SHORT, GL_BYTE, GL_UNSIGNED_BYTE};
  return table[static_cast<size_t>(type)];
}

static inline GLuint GetDataBytes(AttriDataType type) {
  static GLenum table[] = {4, 4, 4, 2, 2, 1, 1};
  return table[static_cast<size_t>(type)];
}

static inline GLenum GetShaderGLType(AttriDataType type, GLubyte component) {
  switch (component) {
  case 1:

    break;
  case 2:
    break;
  case 3:
    break;
  case 4:
    break;
  default:
    return ERR_ENUM;
    break;
  }
}

/*
  Reserved Attributes for Mesh shader

*/
enum VertexAttributes {
  Position,
  Normal,
  TexCoordinate,
  Color,
  BiTangent,
  Tangent,
  Count
};

static inline const std::string GetVertexAttributeName(VertexAttributes attribute) {
  static const std::string table[] = {"vPos", "vNor", "vTex", "vCol", "vBi", "vTan"};
  return table[static_cast<size_t>(attribute)];
}

template <int count = 1> struct AttribBindingBase {
  uint64_t baseLocation[count] = {0};
  uint16_t baseEnableLoc;

#define LOOP_LOC(x) for (int x = 0; x < MAX_VERTEX_ATTRIBUTE_NUM; x++)

  AttribBindingBase() : baseEnableLoc(0) {}
  inline void Clear() {
    std::memset(baseLocation, 0, sizeof(uint64_t) * count);
    baseEnableLoc = 0;
  }
  inline void WriteLoc(GLuint index, GLuint loc, GLuint cnt = 0) {
    const GLuint shift = 4 * index;
    const uint64_t mask = ((uint64_t)0xF) << shift;
    /* overwrite this attr's previous location */
    baseLocation[cnt] = (baseLocation[cnt] & ~mask) | (loc << shift);
    /* mark this attr as enabled */
    baseEnableLoc |= 1 << index;
  }
  inline GLuint ReadLoc(GLuint index, GLuint cnt = 0) {
    return (baseLocation[cnt] >> (4 * index)) & 0xF;
  }
  inline std::vector<GLuint> GetEnabledIndex(GLuint cnt = 0) {
    std::vector<GLuint> indices;
    LOOP_LOC(i) {
      if (baseEnableLoc & (0x1 << i))
        indices.push_back(i);
    }
    return indices;
  }
  inline GLuint GetFirstUnEnabledIndex(GLuint cnt = 0) {
    LOOP_LOC(i) {
      if (!(baseEnableLoc & (0x1 << i))) {
        return i;
      }
    }
    return 0;
  }
  inline GLuint GetUnEnabledLocNum(GLuint cnt = 0) {
    GLuint sum = 0;
    LOOP_LOC(i) {
      if (!(baseEnableLoc & (0x1 << i))) {
        sum++;
      }
    }
    return sum;
  }
  inline GLuint GetEnabledLocNums() {
    return MAX_VERTEX_ATTRIBUTE_NUM - GetUnEnabledLocNum();
  }
  inline bool IsLocExisted(GLuint loc, GLuint cnt = 0) {
    LOOP_LOC(i) {
      if (ReadLoc(cnt, i) == loc)
        return true;
    }
    return false;
  }
  inline void Enable(GLuint index) { baseEnableLoc |= 1 << index; }
  inline void Disable(GLuint index) { baseEnableLoc ^= 1 << index; }
#undef LOOP_LOC
};

struct AttributeBinding : AttribBindingBase<> {
  AttributeBinding() : AttribBindingBase() {}
  inline void WriteAttributeLoc(GLuint index, GLuint loc) { WriteLoc(index, loc); }
  inline GLuint FindUnusedAttribute() { return GetFirstUnEnabledIndex(); }
  inline GLuint ReadAttributeLoc(GLuint index) { return ReadLoc(index); }
  inline GLuint GetCurrentMaxLoc() {
    GLuint max = 0;
    for (int i = 0; i < MAX_VERTEX_ATTRIBUTE_NUM; i++) {
      max = std::max(ReadAttributeLoc(i), max);
    }
    return max;
  }
  inline GLuint GetAttributeSize() { return GetEnabledLocNums(); }
  inline bool IsLocationValid(GLuint loc) { return IsLocExisted(loc); }
  inline std::vector<GLuint> GetEnabledAttributeIndex() { return GetEnabledIndex(); }
  inline void EnableAttribute(GLuint index) { Enable(index); }
  inline void DisableAttribute(GLuint index) { Disable(index); }
}; // namespace Data

struct BindingPoint : AttribBindingBase<> {
  BindingPoint() : AttribBindingBase() {}
  // Create a binding point from a default view
  inline GLuint CreateBindPoint() {
    GLuint index = GetFirstUnEnabledIndex();
    Enable(index);
    return index;
  }

  inline void RemoveBindingPoint(GLuint index) { Disable(index); }
  void AddAttributeLoc(GLuint index, GLuint loc) { WriteLoc(index, loc); }
  inline std::vector<GLuint> GetEnabledBindingPoints() { return GetEnabledIndex(); }
  inline std::vector<GLuint> GetBindingAttributeIndex(GLuint bindingIndex) {}
};

struct AttributeFormat {
  AttributeFormat(const std::string &name, AttriDataType type, GLubyte nc, GLsizei reo)
      : name(name), dataType(type), component(nc), relativeOffset(reo) {}
  AttributeFormat()
      : name(), dataType(AttriDataType::FLOAT32), component(4), relativeOffset(0) {}

  void Clear() {
    name.clear();
    dataType = AttriDataType::FLOAT32;
    component = 4;
    relativeOffset = 0;
  }
  std::string name;
  AttriDataType dataType;
  // TODO: May be calculate by computer
  GLubyte component;
  GLsizei relativeOffset;
};

/*
  Attribute index and binding Index
  Just format settings, no any data transfer
*/

class VertexState {
public:
  class BindingPoint {
  public:
    BindingPoint(bool enabled, GLuint point)
        : _point(point), _vertexAttrib_enabled(enabled) {
      init();
    }
    BindingPoint(GLuint point) : _point(point), _vertexAttrib_enabled(true) { init(); }
    BindingPoint() : _point(0), _vertexAttrib_enabled(true) { init(); }

    // A bindingPoint a create call

    GLint GetID() const { return _point; }

    AttributeFormat &operator[](size_t attributeIndex) {
      return _formats.at(attributeIndex);
    }

    inline void AddAttributeFormat(GLuint attributeIndex, GLuint loc,
                                   const AttributeFormat &format) {
      assert(attributeIndex < MAX_VERTEX_ATTRIBUTE_NUM);
      _attributes[attributeIndex] = loc;
      _formats[attributeIndex] = format;
    }
    inline void AddAttributeFormat(GLuint attributeIndex, const AttributeFormat &format) {
      AddAttributeFormat(attributeIndex, attributeIndex, format);
    }
    inline void AddAttributeFormat(const AttributeFormat &format) {
      AddAttributeFormat(FindUnusedAttribute(), format);
    }
    void RemoveAttributeFormat(GLuint attributeIndex);

    void Clear();
    auto GetEnabledAttributeFormatNum() const { return _attributes.size(); }

    std::vector<GLuint> GetEnabledAttribute();

  private:
    // Init reserved vertex attributes(default is interveal)
    void init();
    GLuint FindUnusedAttribute();
    GLuint _point;
    // enable reserved vertex attribute?
    bool _vertexAttrib_enabled;
    std::array<GLuint, MAX_VERTEX_ATTRIBUTE_NUM> _attributes;
    std::array<AttributeFormat, MAX_VERTEX_ATTRIBUTE_NUM> _formats;
  };

public:
  using RefBP =
      std::array<std::optional<BindingPoint>, MAX_VERTEX_ATTRIBUTE_NUM>::iterator;
  using ConstRefBP =
      std::array<std::optional<BindingPoint>, MAX_VERTEX_ATTRIBUTE_NUM>::const_iterator;

  VertexState() : _id(0) { init(); }

  /*
    Create a binding point with \p enable default vertex attribute
  */
  void CreateBindingPoint();

  void RemoveBindingPoint(size_t i);

  GLuint GetBindingIndex(size_t i) const { return _points.at(i)->GetID(); }

  BindingPoint &operator[](size_t i) { return _points.at(i).value(); }

  void AddAttributeBinding(GLuint attributeIndex, GLuint bindingIndex, GLuint loc,
                           const AttributeFormat &format);
  void AddAttributeBinding(GLuint attributeIndex, const AttributeFormat &format);
  void AddAttributeBinding(const AttributeFormat &format);
  void RemoveAttributeBinding(GLuint attributeIndex, GLuint bindingIndex = 0);
  void Update(GLuint program);

  GLuint GetBindingPointSize() const { return _enabled_points; }

  std::vector<GLuint> GetEnabledAttributeIndex(GLuint bindingIndex);
  GLuint GetID() const { return _id; }

  void Remove() {
    glDeleteVertexArrays(1, &_id);
    _id = 0;
  }

private:
  void init();

  GLuint _id;
  AttributeBinding _bindings;
  GLuint _enabled_points = 0;
  std::array<std::optional<BindingPoint>, MAX_VERTEX_ATTRIBUTE_NUM> _points;
};

} // namespace Data

#endif